蚂蚁图形引擎 Oasis 物理第二弹:物理多后端与组件架构设计

您所在的位置:网站首页 js物理引擎 掘金 蚂蚁图形引擎 Oasis 物理第二弹:物理多后端与组件架构设计

蚂蚁图形引擎 Oasis 物理第二弹:物理多后端与组件架构设计

2023-09-08 07:48| 来源: 网络整理| 查看: 265

作者 - Oasis 团队 - 桐伦

引言

在游戏开发中,物理引擎是一个非常重要的组成部分。在大家的概念当中,在很多游戏当中似乎物理都没有都用到过,初中学习的牛顿定理早就已经不记得了,那么物理引擎到底有什么作用呢?事实上,在一般用于游戏的物理引擎当中,物理的过程是空间稀疏而非稠密的,即物理引擎不考虑例如空气之类的流体,而主要关注刚性物体。因此,物理引擎会包括两个部分,即:

触发

物理响应

在大多数的游戏当中,都会有触发的逻辑,例如,一个角色往前跑,碰到一个障碍物,自主的跳了过去。实际上,这一过程包含了几个步骤:

调用奔跑的动画

每一帧碰撞检测,检测到角色与障碍物碰撞,触发事件

事件调用一个新的动画,即跳跃

碰撞结束后触发事件

调用下落的动画

在前端的游戏引擎当中,比较常用的物理引擎是 ammo.js, cannon.js,前者使用 WebIDL 将 Bullet 编译到 WASM,后者则是纯 JavaScript 写的物理引擎。从功能上对比,二者都不如游戏行业广为使用的 PhysX 来的强大。在 Oasis 当中,我们使用同为 Emscripten 工具包的 Embind,将 PhysX4.1 编译为 WASM,并且基于此封装物理组件,以提供包括触发器,raycast,sweep,刚体,约束体,车辆,布料等一系列物理能力。在《 Oasis 物理第一弹:基于 WebAssembly 的 PhysX 跨平台编译与 PVD 联调》当中,我们已经介绍了如何将 C++ 编写的 PhysX 编译为 .wasm 二进制文件和方便导入的 JavaScript 胶水文件。这一篇文章中,我们将介绍 Oasis 中如何应用 PhysX 以及为其设计扩展性强的物理组件架构,使得在未来可以增加更多的物理后端。

基于分包的整体架构

由于 PhysX 的 .wasm 文件接近2兆,并且有很多场景下用户并不需要处理非常复杂的物理事件,而只需要触发器和射线检测这些方法,因此,全部依赖 PhysX 在一些场景下就不太合适。因此,Oasis 在物理后端做了分包的处理,即在需要复杂物理响应的场景应用 PhysX,在简单的场景中使用引擎现有的能力,但这二者都用一种接口进行封装,使得可以按需无缝切换。最终 Oasis 的物理组件呈现如下的分包结构:

一共分为三层

@core/physics:Engine 中保存一个 PhysicsManager 对象,用于实现 raycast 以及管理物理组件的创建与销毁。 @design/physics:纯接口包,桥接 core 包和具体物理实现 @physics-xxx:各种符合 @design/physics 接口的物理实现包

3.1. physics-physx:基于 PhysX 的物理组件

3.2. physics-lite:轻量级物理组件,当前包括 Oasis 0.5里程碑中包含的碰撞器与 raycast 方法

本文将在物理分包的基础上,围绕 PhysX 自身架构介绍 Oasis 相关的实现和调用逻辑。

加载WASM

基于多后端的设计,在引擎初始化时可以选择性载入物理包,因此在 Engine 初始化时增加了一个 IPhysics 类型的可选参数。对于需要物理组件的代码,需要将代码写成:

const engine = new WebGLEngine("canvas", LitePhysics);

由于 .wasm 文件的加载必须是异步的,且必须在初始化所有物理组件之前,因此 PhysXPhyscis 提供了一个初始化函数,并且所有引擎初始化逻辑都必须写在回调函数当中,例如:

PhysXPhysics.init().then(() => { const engine = new WebGLEngine("canvas", PhysXPhysics); })

传入引擎的 IPhysics 会被保存下来,通过装饰器 StaticInterfaceImplement 强制要求所有的成员方法都是静态的,使得物理组件都可以直接调取其中的静态方法进行初始化。对于用户自己实现的第三方物理包,也推荐采用以下形式进行约束:

@StaticInterfaceImplement() export class PhysXPhysics {} 类型体系

在 Oasis 的物理组件实现过程中,主要参考了PhysX的类型体系来组织 @design/physics 当中的接口设计。然后根据接口再构造具体实现以及 @core/physics 当中的用户组件实现。但事实上,这种架构广泛存在于包括 Bullet,Box2d 等主流物理引擎当中,具有通用性。因此,本文将围绕 PhysX 的类型体系,展示他们是如何组织在一起的:

上图展示了 PhysX 与 Oasis 结合的具体逻辑,图中主要有个两层次的信息:

红色标签表示的是 PhysX 的类型,从构造 PxFoundation 开始,逐渐构建 PxShape,PxActor,PxScene,然后在 PxScene 中调用 simulate 方法或者 raycast 方法。另外,蓝色线表示的是 PhysX 对象的构造顺序。 绿色标签表示的 Oasis 引擎对其封装的类型,对于 PxShape 封装成 ColliderShape,对 PxActor 封装成 Collider,PhyscisManager 封装 PxScene。另外,红色线表示 Oasis 调用引擎算法的顺序,主要是调用 PxScene 中的 simulate 方法,然后出发脚本中的事件。

对上述逻辑的一个最为根本的抽象是:*物理场景中有两类 Collider,即 Dynamic 和 Static,每个 Collider 都是ColliderShape 的一个容器,因此可以通过组合的方式构造成复杂的碰撞器外形。 *因此,对于物理组件的一个标准创建代码如下:

const capsuleShape = new CapsuleColliderShape(); capsuleShape.radius = radius; capsuleShape.height = height; const capsuleCollider = cubeEntity.addComponent(StaticCollider); capsuleCollider.addShape(capsuleShape);

基于此我们重新规划了物理组件的接口,做了如下的设计:

ColliderShape

构造物理组件的第一步是创建 ColliderShape,在 Oasis 当中,ColliderShape 表示碰撞器的外形,包含了 Plane,Box,Sphere,Capsule。ColliderShape 都可以设置局部位置和尺度大小。由于 Collider 可以绑定多个 Shape,属于 Shape 的集合,因此,所有 Shape 的位置和旋转属性都是相对于 Collider 而言。

接口层 IColliderShape

在上述原则下,我们的接口层包含了如下方法:

export interface IColliderShape { /** * Set unique id of the collider shape. * @param id - The unique index */ setUniqueID(id: number): void; /** * Set local position. * @param position - The local position */ setPosition(position: Vector3): void; /** * Set world scale of shape. * @param scale - The scale */ setWorldScale(scale: Vector3): void; }

每一个 ColliderShape 都会有唯一的ID标识,该标识主要是为了桥接物理引擎中的 ColliderShape 对象,以及Oasis 引擎中的 Entity。在 PhysX 当中,事件的触发都是围绕着 Shape 展开的,因此,通过该ID可以很快知道到底是哪两个 Entity 之间发生了碰撞事件,由此可以执行对应的组件,例如脚本。

Collider

加下来就要创建 Collider 组件,在物理系统当中 Collider 是用户唯一需要创建的组件。Collider 本身没有形状,但分为两种:

StaticCollider:对应 PxStaticActor,一般不会随时间移动,主要作为触发器和静态碰撞器。 DynamicCollider:对应 PxDynamicActor,一般会随时间移动,受到用户控制,或者响应物理运动。

Collider 上也可以设置位置和旋转,但在 Oasis 当中该属性对用户是不可见的,渲染引擎会自动同步物理引擎,用户只可以修改 ColliderShape 上的相对位置,或者调整Entity的姿态来控制碰撞器移动。

接口层 ICollider

尽管 Collider 的姿态并不暴露给开发者,但在接口层中依旧需要对应接口:

export interface ICollider { /** * Set global transform of collider. * @param position - The global position * @param rotation - The global rotation */ setWorldTransform(position: Vector3, rotation: Quaternion): void; /** * Get global transform of collider. * @param outPosition - The global position * @param outRotation - The global rotation */ getWorldTransform(outPosition: Vector3, outRotation: Quaternion): void; /** * Add collider shape on collider. * @param shape - The collider shape attached */ addShape(shape: IColliderShape): void; /** * Remove collider shape on collider. * @param shape - The collider shape attached */ removeShape(shape: IColliderShape): void; } PhysicsManager

Collider 创建后需要被添加到物理场景当中,PhysicsManager 用来管理物理场景,并且根据其中保存的 Actor 来更新物理场景,例如约束运动或者触发碰撞检测事件。在 Oasis 当中,PhysicsMananger 默认构造, 并且在创建 Collider 组件时自动将其添加到 PhysicsMananger 当中进行管理。同时,PhysicsManage r还包含射线检测方法:

engine.physicsManager.raycast(ray, Number.MAX_VALUE, Layer.Everything, hit); 接口层 IPhysicsMananger

该接口定义了物理场景的管理器类,主要包含:

export interface IPhysicsManager { /** * Add ICollider into the manager. * @param collider - StaticCollider or DynamicCollider. */ addCollider(collider: ICollider): void; /** * Remove ICollider. * @param collider - StaticCollider or DynamicCollider. */ removeCollider(collider: ICollider): void; /** * Call on every frame to update pose of objects. * @param elapsedTime - Step time of update. */ update(elapsedTime: number): void; /** * Casts a ray through the Scene and returns the first hit. * @param ray - The ray * @param distance - The max distance the ray should check * @param outHitResult - If true is returned, outHitResult will contain more detailed collision information * @returns Returns True if the ray intersects with a collider, otherwise false */ raycast( ray: Ray, distance: number, outHitResult?: (shapeUniqueID: number, distance: number, point: Vector3, normal: Vector3) => void ): boolean; } 物理与渲染的同步逻辑

经过了这一里程碑的重构,物理事件与其他事件的执行剥离开来,更加理清了物理事件与其他脚本事件的关系:

if (scene) { componentsManager.callScriptOnStart(); if (this.physicsManager) { componentsManager.callColliderOnUpdate(); this.physicsManager.update(deltaTime); componentsManager.callColliderOnLateUpdate(); } componentsManager.callScriptOnUpdate(deltaTime); componentsManager.callAnimationUpdate(deltaTime); componentsManager.callScriptOnLateUpdate(deltaTime); this._render(scene); }

总体上,与物理相关的更新逻辑分为三步:

callColliderOnUpdate:首先,如果 Entity 被用户手动更新,例如执行了脚本逻辑,那么需要将新的姿态同步到物理引擎当汇总。 physicsManager.update:其次执行物理引擎的更新逻辑,包括碰撞检测,触发事件,完成物理响应等等。 callColliderOnLateUpdate:最后,对于 DynamicCollider,他的姿态会受到物理响应的影响,于是要重新将姿态同步到 Entity 上,使得渲染的物体也能发生运动。 总结

这篇文章简单介绍了我们基于 PhysX 实现物理组件,分为三层架构,用户可以通过实现 @design/physics 当中的接口来实现自己的物理底层。实际上,这种面向接口的架构模式在将来引入 WebGPU 等新的渲染 API 时同样适用,这样一来引擎和底层 API 实现了彻底的分离,最终实现跨平台运行。

在分包逻辑上,我们设计了 ColliderShape,Collider,PhysicsManager 等组件接口,并且实现了基于 PhysX 的物理后端,同时提供了轻量级的物理后端 @Physics-Lite 以支持轻量级的场景。

在下一个里程碑当中,我们将会进一步强化 DynamicCollider,增加物理响应,物理约束,并且增加角色控制器等组件。借助这些组件将很容易开发出弹球,射击类的应用,敬请期待。同时《Oasis物理》系列也将会继续围绕着 PhysX 和物理引擎介绍更多内容。在《Oasis 物理第三弹》当中,我们将继续围绕着这篇文章介绍的物理架构,以 @Physics-Lite 包为例介绍物理引擎实现过程中的技术细节。

写在最后

欢迎大家 star 我们的 github 仓库,也可以随时关注我们后续 v0.7 的规划,也可以在 issues 里给我们提需求和问题。开发者可以加入到我们的钉钉群里来跟我们吐槽和探讨一些问题:

无论你是渲染、TA 、Web 前端或是游戏方向,只要你和我们一样,渴望实现心中的绿洲,欢迎投递简历到 [email protected]。岗位描述详见:www.yuque.com/oasis-engin…



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3